using System;
using System.Configuration;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Text;
using System.Xml;

namespace Pegasus.Configuration
{
	/// <summary>
	/// Allows extension of any configuration section that is accessed using ConfigurationSettings.AppSettings 
	/// to enable mutiple configuration sets to be held in the same file, and the correct one picked at runtime
	/// by looking at settings unique to the runtime environment (in this implementation the hostname)
	/// </summary>
	/// <example><code>
	///&lt;configuration&gt;
	///    &lt;configSections&gt;
	///        &lt;remove name="appSettings"/&gt;
	///        &lt;section name="appSettings" type="Pegasus.Configuration.ConfigurationSettingsEx, Pegasus.Library"/&gt;
	///    &lt;/configSections&gt;
	/// 
	///    &lt;appSettings&gt;
	/// 
	///        &lt;!-- Use the DevLevel property from machine.config or webenv.config --&gt;
	///        &lt;devLevelConfig level="LOC"&gt;
	///            &lt;include setname="Dev"/&gt;
	///        &lt;/devLevelConfig&gt;
	///        
	///        &lt;devLevelConfig level="DEV"&gt;
	///            &lt;include setname="Dev"/&gt;
	///        &lt;/devLevelConfig&gt;
	///        
	///        &lt;devLevelConfig level="STAGE"&gt;
	///            &lt;include setname="Stage"/&gt;
	///        &lt;/devLevelConfig&gt;
	///        
	///        &lt;devLevelConfig level="LIVE"&gt;
	///            &lt;include setname="Live"/&gt;
	///        &lt;/devLevelConfig&gt;
	///        
	///        &lt;!-- 
	///            Machine specific settings. This should only be used for development systems and
	///            should not be used for production systems. Use devLevelConfig.
	///        --&gt;
	///        &lt;machineConfig hostname="machine1, machine2, machine3"&gt;
	///            &lt;include setname="Dev"/&gt;
	///        &lt;/machineConfig&gt;
	/// 
	///        &lt;machineConfig hostname="machine4"&gt;
	///            &lt;include setname="Stage"/&gt;
	///        &lt;/machineConfig&gt;
	///
	///        &lt;!-- common to all --&gt;
	///        &lt;add key="key5" value="value5"/&gt;
	///        &lt;add key="key6" value="value6"/&gt;
	/// 
	///        &lt;!-- Dev environment --&gt;
	///        &lt;setConfig setname="Dev"&gt;
	///            &lt;add key="environmentName" value="dev"/&gt;
	///            &lt;add key="key1" value="dev_value1"/&gt;
	///            &lt;add key="key2" value="dev_value2"/&gt;
	///        &lt;/setConfig&gt;
	///
	///        &lt;!-- Stage environment --&gt;
	///        &lt;setConfig setname="Stage"&gt;
	///            &lt;add key="key1" value="stage_value1"/&gt;
	///            &lt;add key="key2" value="stage_value2"/&gt;
	///        &lt;/setConfig&gt;
	///
	///        &lt;!-- Live enivironment --&gt;
	///        &lt;setConfig setname="Live"&gt;
	///            &lt;add key="environmentName" value="Live"/&gt;
	///            &lt;add key="key3" value="live_value" /&gt;
	///            &lt;include set="Stage" /&gt;
	///            &lt;add key="key6" value="overridden_live_value6"/&gt;
	///        &lt;/setConfig&gt;
	///    &lt;/appSettings&gt;
	/// &lt;/configuration&gt;
	///</code>
	/// This config file would be parsed and presented to the application 
	/// in the normal way. No code beyond the config file should need to 
	/// be changed.
	/// </example>
	public class ConfigurationSettingsEx : IConfigurationSectionHandler
	{
		// The dev level that the machine is configured for.
		private string m_devLevel = null;

		/// <summary>
		/// Default Constructor
		/// </summary>
		public ConfigurationSettingsEx()
		{
		}

		/// <summary>
		///	Parses an <c>XmlNode</c> from a configuration file to produce an XmlNode that is compatible with the original 
		///	<c>IConfigurationSectionHander</c> for the section. Instantiates and instance of the original handler and passes in 
		///	the new <c>XmlNode</c> to get back an <c>object</c> of the same type as the original handler would have returned.
		/// </summary>
		/// <param name="inParent"></param>
		/// <param name="inConfigContext"></param>
		/// <param name="inSection">The <c>XmlNode</c> from the config file</param>
		/// <returns>
		///	<c>object</c> of the same type as the original <c>IConfigurationSectionHandler</c> returns
		///	</returns>
		public virtual object Create( Object inParent, Object inConfigContext, XmlNode inSection )
		{
			// Create the new section node.
			XmlNode theNewSection = inSection.OwnerDocument.CreateNode( XmlNodeType.Element, inSection.Name, inSection.NamespaceURI );

			Stack theProcessStack = new Stack();
			String theParentHandlerType = ParseSection( theNewSection, inSection, theProcessStack );

			// Create an instance of the original handler and call it's create method returning the result.
			// As some of the handler we might try to call may be marked as private/internal we need to 
			// give ourselves permission to access these objects via reflection
			CodeAccessPermission theCodeAccessPermission = new ReflectionPermission( PermissionState.Unrestricted );
			theCodeAccessPermission.Assert();
			
			Type theParentHandlerTypeObject = Type.GetType( theParentHandlerType );
			if( theParentHandlerTypeObject == null )
			{
				throw new ConfigurationErrorsException( "ConfigurationSettingsEx: Failed to GetType for " + theParentHandlerType );
			}

			IConfigurationSectionHandler theParentHandler = (IConfigurationSectionHandler) Activator.CreateInstance( 
				theParentHandlerTypeObject, (BindingFlags)0x234, null, null, null );
			
			CodeAccessPermission.RevertAssert();

			return theParentHandler.Create( inParent, inConfigContext, theNewSection );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inSetNode"></param>
		/// <param name="ioProcessStack"></param>
		private void PushToStack( XmlNode inSetNode, Stack ioProcessStack )
		{
			// Get the name of the set, see if it is already on the process stack,
			// if it is then we have an infinte loop in our processing so we will throw an exception
			// otherwise push it onto the stack to show which nodes we have processed through
			XmlAttribute theSetNameAttr = inSetNode.Attributes[ "setname" ];
			String theSetName = null;
			
			if( theSetNameAttr != null )
			{
				theSetName = theSetNameAttr.Value;

				if( ioProcessStack.Contains( theSetName ) )
				{
					String st = theSetName;
					while( ioProcessStack.Count > 0 )
					{
						st = ioProcessStack.Pop().ToString() + " --> " + st;
					}

					throw new ConfigurationErrorsException( "ConfigurationSettingsEx: Infinite recursion path found while processing setConfig: " + st );
				}
				else
				{
					ioProcessStack.Push( theSetName );
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsParentHandlerNode( XmlNode inNode )
		{
			if( IsCommentOrWhitespaceNode( inNode ) )
			{
				return false;
			}

			if( IsMachineConfigNode( inNode ) )
			{
				return false;
			}

			if( IsDefaultMachineConfigNode( inNode ) )
			{
				return false;
			}

			if( IsDevLevelConfigNode( inNode ) )
			{
				return false;
			}

			if( IsIncludeNode( inNode ) )
			{
				return false;
			}

			if( IsSetConfigNode( inNode ) )
			{
				return false;
			}

			return true;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsCommentOrWhitespaceNode( XmlNode inNode )
		{
			return inNode.Name == "#whitespace" || inNode.Name == "#comment";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsMachineConfigNode( XmlNode inNode )
		{
			return inNode.Name == "machineConfig";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsDefaultMachineConfigNode( XmlNode inNode )
		{
			return inNode.Name == "defaultMachineConfig";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsDevLevelConfigNode( XmlNode inNode )
		{
			return inNode.Name == "devLevelConfig";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsSetConfigNode( XmlNode inNode )
		{
			return inNode.Name == "setConfig";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		private bool IsIncludeNode( XmlNode inNode )
		{
			return inNode.Name == "include";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ioNewSection"></param>
		/// <param name="inSection"></param>
		/// <param name="ioProcessStack"></param>
		/// <returns></returns>
		private string ParseSection( XmlNode ioNewSection, XmlNode inSection, Stack ioProcessStack )
		{
			bool foundMachineConfig = false;
			bool foundDevLevelConfig = false;
			XmlNode defaultMachineConfigNode = null;

			foreach( XmlNode node in inSection.ChildNodes )
			{
				if( IsParentHandlerNode( node ) )
				{
					ioNewSection.AppendChild( node.CloneNode( true ) );
				}
			}

			foreach( XmlNode node in inSection.ChildNodes )
			{
				if( IsMachineConfigNode( node ) )
				{
					if( IamHostMachine( node ) )
					{
						if( !foundMachineConfig )
						{
							ParseMachineConfig( ioNewSection, node, ioProcessStack );
							foundMachineConfig = true;
						}
						else if( foundMachineConfig && IamHostMachine( node ) )
						{
							throw new Exception( "ConfigurationSettingsEx: ERROR! There are multiple config settings for this machine!" );
						}
					}
				}
				else if( IsDefaultMachineConfigNode( node ) )
				{
					defaultMachineConfigNode = node;
				}
				else if( IsDevLevelConfigNode( node ) )
				{
					if( IamInDevLevel( node ) )
					{
						if( !foundDevLevelConfig )
						{
							ParseDevLevelConfig( ioNewSection, node, ioProcessStack );
							foundDevLevelConfig = true;
						}
						else
						{
							throw new Exception( "ConfigurationSettingsEx: ERROR! There are multiple config settings for this envrionment" );
						}
					}
				}
			}

			if( !foundMachineConfig )
			{
				if( defaultMachineConfigNode != null )
				{
					ParseDebugMachineConfig( ioNewSection, defaultMachineConfigNode, ioProcessStack );
				}
			}

			string theParentHandlerType = null;
			XmlAttribute theParentHandlerTypeAttribute = inSection.Attributes[ "parentHandlerType" ];
			if( theParentHandlerTypeAttribute != null )
			{
				theParentHandlerType = theParentHandlerTypeAttribute.Value;
			}
			else
			{
				Assembly asm = Assembly.GetAssembly( typeof( System.Configuration.NameValueFileSectionHandler ) );
				theParentHandlerType = "System.Configuration.NameValueFileSectionHandler, " + asm.FullName;
					//System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
			}

			foreach( XmlAttribute attr in inSection.Attributes )
			{
				if( attr.Name != "parentHandlerType" )
				{
					ioNewSection.Attributes.Append( (XmlAttribute) attr.CloneNode( true ) );
				}
			}

			return theParentHandlerType;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ioNewSection"></param>
		/// <param name="inDefaultMachineConfigNode"></param>
		/// <param name="ioProcessStack"></param>
		private void ParseDebugMachineConfig( XmlNode ioNewSection, XmlNode inDefaultMachineConfigNode, Stack ioProcessStack )
		{
			ParseMachineConfig( ioNewSection, inDefaultMachineConfigNode, ioProcessStack );
		}

		/// <summary>
		/// Processes a setConfig <c>XmlNode</c> (of the type we are handling) 
		/// which contains configuration information.
		/// </summary>
		/// <param name="ioNewSection"></param>
		/// <param name="inSetNode"></param>
		/// <param name="ioProcessStack"></param>
		private void ParseSetConfig( XmlNode ioNewSection, XmlNode inSetNode, Stack ioProcessStack )
		{
			PushToStack( inSetNode, ioProcessStack );
			
			// we are clear to process this node so for each of the 
			// contained node call the relevant processing step
			foreach( XmlNode node in inSetNode.ChildNodes )
			{	
				if( IsIncludeNode( node ) )
				{
					ParseInclude( ioNewSection, node, ioProcessStack );
				}
				else if( IsParentHandlerNode( node ) )
				{
					ioNewSection.AppendChild( node.CloneNode( true ) );
				}
			}

			PopFromStack( ioProcessStack );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ioProcessStack"></param>
		private void PopFromStack( Stack ioProcessStack )
		{
			ioProcessStack.Pop();
		}

		/// <summary>
		/// Processes an include node, the set referenced by the node but have 
		/// the same parent as the parent of this include node. However the names 
		/// of configSets must be unique in the entire appSettings block due to the 
		/// way we detect loops if the parsing
		/// </summary>
		/// <param name="ioNewSection"></param>
		/// <param name="inIncludeNode"></param>
		/// <param name="inProcessStack"></param>
		private void ParseInclude( XmlNode ioNewSection, XmlNode inIncludeNode, Stack inProcessStack )
		{
			XmlAttribute theSetName = inIncludeNode.Attributes[ "setname" ];

			if( theSetName != null )
			{
				// find the setConfig specified - must have the same parent as the parent of this include node
				XmlNode theSetNode = inIncludeNode.SelectSingleNode( "../../setConfig[@setname=\"" + theSetName.Value + "\"]" );
				if( theSetNode != null )
				{
					ParseSetConfig( ioNewSection, theSetNode, inProcessStack );
				}
			}
			else
			{
				throw new ConfigurationErrorsException( "ConfigurationSettingsEx: Missing 'setname' attribute in configuration: " + inIncludeNode.OuterXml );
			}
		}

		/// <summary>
		/// Parses a machineConfig node. 
		/// If the node is found to relate to the current hostname then all of contained
		/// include elements are processes
		/// </summary>
		/// <param name="ioNewSection">the new node we are building</param>
		/// <param name="inMachineConfigNode">machineConfig node to process</param>
		/// <param name="ioProcessStack">the process stack to make sure there is no recursion.</param>
		private void ParseMachineConfig( XmlNode ioNewSection, XmlNode inMachineConfigNode, Stack ioProcessStack )
		{
			// we are clear to process this node so for each of the 
			// contained node call the relevant processing step
			foreach( XmlNode node in inMachineConfigNode.ChildNodes )
			{	
				if( IsIncludeNode( node ) )
				{
					ParseInclude( ioNewSection, node, ioProcessStack );
				}
				else if( IsParentHandlerNode( node ) )
				{
					ioNewSection.AppendChild( node.CloneNode( true ) );
				}
			}
		}

		/// <summary>
		/// Parses a devLevelConfig node. 
		/// If the node is found to relate to the current dev, staging, or live environment then all of contained
		/// include elements are processes
		/// </summary>
		/// <param name="ioNewSection">The new node we are building</param>
		/// <param name="inConfigNode">MachineConfig node to process</param>
		/// <param name="ioProcessStack">The process stack to make sure there is no recursion.</param>
		private void ParseDevLevelConfig( XmlNode ioNewSection, XmlNode inConfigNode, Stack ioProcessStack )
		{
			// we are clear to process this node so for each of the 
			// contained node call the relevant processing step
			foreach( XmlNode node in inConfigNode.ChildNodes )
			{	
				if( IsIncludeNode( node ) )
				{
					ParseInclude( ioNewSection, node, ioProcessStack );
				}
				else if( IsParentHandlerNode( node ) )
				{
					ioNewSection.AppendChild( node.CloneNode( true ) );
				}
			}
		}

		/// <summary>
		/// Check if we want to process the current machineConfig node.
		/// </summary>
		/// <param name="inMachineConfigNode">The configMap node we are deciding whether or not to process</param>
		/// <returns><c>true</c> if we should process this node, otherwise <c>false</c></returns>
		protected virtual bool IamHostMachine( XmlNode inMachineConfigNode )
		{
			XmlAttribute attrib = inMachineConfigNode.Attributes[ "hostname" ];
			if( attrib != null )
			{
				string value = attrib.Value;
				if( value != null && value.Length > 0 )
				{
					string[] hostnames = value.Split( ',' );
					foreach( string hostname in hostnames )
					{
						if( CheckHostname( hostname ) )
						{
							return true;
						}
					}
				}
			}

			attrib = inMachineConfigNode.Attributes[ "hostnameRegEx" ];
			if( attrib != null )
			{
				string value = attrib.Value;
				if( value != null && value.Length > 0 )
				{
					if( CheckHostnameRegEx( value ) )
					{
						return true;
					}
				}
			}

			return false;
		}

		/// <summary>
		/// Check to see if we are in the proper config environment.
		/// </summary>
		/// <param name="inNode"></param>
		/// <returns></returns>
		protected bool IamInDevLevel( XmlNode inNode )
		{
			string devLevel = null;
			XmlAttribute attrib = inNode.Attributes[ "level" ];
			if( attrib != null )
			{
				devLevel = attrib.Value;
			}

			if( devLevel != null )
			{
				return GetCurrentDevLevel() == devLevel;
			}

			return false;
		}

		/// <summary>
		/// Check the hostname specified against the current machinename
		/// used to see if we want to process the current machineConfig node
		/// </summary>
		/// <param name="inHostName">The hostname from the machineConfig node</param>
		/// <returns><c>true</c> - match, <c>false</c> - no match</returns>
		protected bool CheckHostname( string inHostName )
		{
			if( inHostName != null )
			{
				return Environment.MachineName.ToLower().Equals( inHostName.ToLower() );
			}

			return false;
		}

		/// <summary>
		/// Check if the hostname matches the specified regular expression.
		/// used to see if we want to process the current machineConfig node
		/// </summary>
		/// <param name="inRegExHostName">The hostname from the machineConfig node</param>
		/// <returns><c>true</c> - match, <c>false</c> - no match</returns>
		protected bool CheckHostnameRegEx( string inRegExHostName )
		{
			if( inRegExHostName != null )
			{
				Regex theRegex = new Regex(inRegExHostName, RegexOptions.IgnoreCase);
				return theRegex.IsMatch( Environment.MachineName );
			}

			return false;
		}

		/// <summary>
		/// Get the current DevLevel attribute located in the machine.config or webenv.config files,
		/// or a local webenv.config 
		/// </summary>
		/// <returns>The current dev level string.</returns>
		private string GetCurrentDevLevel()
		{
			if( m_devLevel == null )
			{
				string netConfigDir = EnvironmentEx.DotNetConfigurationDirectory;

				// See if the value is in the machine.config file.
				string filename = Path.Combine( netConfigDir, "machine.config" );
				if( File.Exists( filename ) )
				{
					m_devLevel = GetDevLevelFromFile( filename );
				}

				// See if the webenv.config file is in the .NET config directory
				filename = Path.Combine( netConfigDir, "webenv.config" );
				if( File.Exists( filename ) )
				{
					m_devLevel = GetDevLevelFromFile( filename );
				}

				// See if the webenv.config file is in the windows system directory
				filename = Path.Combine( Environment.SystemDirectory, "webenv.config" );
				if( File.Exists( filename ) )
				{
					m_devLevel = GetDevLevelFromFile( filename );
				}

				// See if the webenv.config is in the startup directory
				filename = Path.Combine( Environment.CurrentDirectory, "webenv.config" );
				if( File.Exists( filename ) )
				{
					m_devLevel = GetDevLevelFromFile( filename );
				}
			}

			return m_devLevel;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="filename"></param>
		/// <returns></returns>
		private string GetDevLevelFromFile( string filename )
		{
			// Load the file
			XmlDocument doc = new XmlDocument();
			doc.Load( filename );

			// Get all the add nodes
			XmlNode devLevelNode = doc.SelectSingleNode( "configuration/appSettings/add[@key=\"DevLevel\"][last()]" );
			if( devLevelNode != null )
			{
				XmlAttribute attrib = devLevelNode.Attributes[ "value" ];
				if( attrib != null )
				{
					return attrib.Value;
				}
			}

			return string.Empty;
		}
	}
}
